/*
 * MIT License
 *
 * Copyright (c) 2021 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.seaboot.word;

import cn.seaboot.commons.core.Converter;
import cn.seaboot.commons.file.IOUtils;
import cn.seaboot.commons.lang.Warning;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;

import java.io.File;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * 代码中使用到了StringTokenizer，暂无修改的计划
 *
 * @author Mr.css
 * @version 2019/08/06 10:46
 * @version 2020/05/20 10:46 解决段落中有多个占位符只替换第一个的问题
 * @version 2020/11/23 17:36 优化代码，可以替换段落中所有的占位符
 */
public class WordUtils {

    private WordUtils() {
    }


    /**
     * 渲染文档中段落的占位符
     *
     * @param doc     文档对象
     * @param dataMap 数据
     */
    public static void replaceTag(XWPFDocument doc, Map<String, ?> dataMap) {
        if (dataMap != null) {
            Iterator<XWPFParagraph> paraIt = doc.getParagraphsIterator();
            //文档中所有段落
            while (paraIt.hasNext()) {
                XWPFParagraph para = paraIt.next();
                replace(para, dataMap);
            }
        }
    }

    /**
     * 渲染某个表格内的所有占位符
     *
     * @param table  -
     * @param params 渲染参数
     */
    public static void renderTable(XWPFTable table, Map<String, Object> params) {
        List<XWPFTableRow> rowList = table.getRows();
        if (rowList != null) {
            for (XWPFTableRow row : rowList) {
                List<XWPFTableCell> cellList = row.getTableCells();
                for (XWPFTableCell cell : cellList) {
                    List<XWPFParagraph> paras = cell.getParagraphs();
                    if (paras != null && !paras.isEmpty()) {
                        for (XWPFParagraph para : paras) {
                            replace(para, params);
                        }
                    }
                }
            }
        }
    }

    /**
     * 遍历段落中的字符，替换占位符
     * 占位符限定为{}
     *
     * @param para 段落
     * @param map  参数
     */
    private static void replace(XWPFParagraph para, Map<String, ?> map) {
        String paraText = para.getParagraphText();
        StringTokenizer tokens = new StringTokenizer(paraText, "{}", true);
        boolean escaped = false;
        while (tokens.hasMoreTokens()) {
            String token = tokens.nextToken();
            if (!escaped && "{".equals(token)) {
                escaped = true;
            } else if (escaped && "}".equals(token)) {
                escaped = false;
            } else if (escaped) {
                String place = "{" + token + "}";
                Object value = map.get(token);
                replaceParagraphValue(para, place, value);
            }
        }
    }


    /**
     * 替换段落中的占位符
     *
     * @param para  段落
     * @param place 占位符
     * @param value 值，可以是{@link DocPicture}也可以是{@link List<DocPicture>}，或者是{@link String}
     */
    @SuppressWarnings(Warning.UNCHECKED)
    private static void replaceParagraphValue(XWPFParagraph para, String place, Object value) {
        if (value == null) {
            //对象为空，清除占位符
            replaceText(para, place, "");
        } else {
            if (value instanceof String) {
                //字符串
                replaceText(para, place, Converter.toString(value));
            } else if (value instanceof DocPicture) {
                //图片
                DocPicture picture = (DocPicture) value;
                replacePicture(para, place, picture);
            } else if (value instanceof List) {
                //图片集合
                List<DocPicture> picture = (List<DocPicture>) value;
                replacePictureList(para, place, picture);
            } else {
                //其它泛型
                replaceText(para, place, Converter.toString(value));
            }
        }
    }

    /**
     * 替换段落中的占位符：文本类型
     *
     * @param para    段落
     * @param place   占位符
     * @param content 内容
     */
    private static void replaceText(XWPFParagraph para, String place, String content) {
        TextSegment textSegment = para.searchText(place, new PositionInParagraph());
        if (textSegment != null) {
            String color = null;
            Double fontSize = -1d;
            String fontFamily = null;
            // 先删除占位符
            for (int i = textSegment.getEndRun(); i >= textSegment.getBeginRun(); i--) {
                XWPFRun curRun = para.getRuns().get(i);
                color = curRun.getColor();
                fontFamily = curRun.getFontFamily();
                fontSize = curRun.getFontSizeAsDouble();
                para.removeRun(i);
            }
            // 在原占位符起始位置插入实际值
            XWPFRun newRun = para.insertNewRun(textSegment.getBeginRun());
            if (color != null) {
                newRun.setColor(color);
            }
            if (fontSize > 0) {
                newRun.setFontSize(fontSize);
            }
            if (fontFamily != null) {
                newRun.setFontFamily(fontFamily);
            }
            newRun.setText(content);
        }
    }

    /**
     * 替换段落中的占位符：图片类型
     *
     * @param para    段落
     * @param place   占位符
     * @param picture 图片对象
     */
    private static void replacePicture(XWPFParagraph para, String place, DocPicture picture) {
        replacePicture(para, place, picture.getFile(),
                picture.getDesc(), picture.getPictureType(), picture.getWidth(), picture.getHeight());
    }

    /**
     * 替换段落中的占位符：图片类型
     *
     * @param para      段落
     * @param place     占位符
     * @param file      图片资源文件
     * @param imageType E.G.:Document.PICTURE_TYPE_PNG
     * @param width     宽
     * @param height    高
     */
    private static void replacePicture(XWPFParagraph para, String place, File file, String desc, int imageType, int width, int height) {
        TextSegment textSegment = para.searchText(place, new PositionInParagraph());
        if (textSegment != null) {
            // 先删除占位符
            for (int i = textSegment.getEndRun(); i >= textSegment.getBeginRun(); i--) {
                para.removeRun(i);
            }
            // 在原占位符起始位置插入实际值
            XWPFRun newRun = para.insertNewRun(textSegment.getBeginRun());
            try (InputStream is = IOUtils.openFileInputStream(file)) {
                newRun.addPicture(is, imageType, desc, Units.toEMU(width), Units.toEMU(height));
            } catch (Exception e) {
                throw new WordException(String.format("在Word中插入%s图片时出错", place), e);
            }
        }
    }

    /**
     * 替换段落中的占位符：图片集合类型
     *
     * @param para  段落
     * @param place 占位符
     * @param list  图片集合
     */
    private static void replacePictureList(XWPFParagraph para, String place, List<DocPicture> list) {
        TextSegment textSegment = para.searchText(place, new PositionInParagraph());
        if (textSegment != null) {
            // 先删除占位符
            for (int i = textSegment.getEndRun(); i >= textSegment.getBeginRun(); i--) {
                para.removeRun(i);
            }
            // 在原占位符起始位置插入实际值
            XWPFRun newRun = para.insertNewRun(textSegment.getBeginRun());
            for (DocPicture pic : list) {
                try (InputStream is = IOUtils.openFileInputStream(pic.getFile())) {
                    newRun.addPicture(is, pic.getPictureType(), pic.getDesc(), Units.toEMU(pic.getWidth()), Units.toEMU(pic.getHeight()));
                } catch (Exception e) {
                    throw new WordException(String.format("在Word中插入%s图片时出错", place), e);
                }
            }
        }
    }

    /**
     * 在段落位置追加一张图片
     *
     * @param para    段落
     * @param picture 图片对象
     */
    private static void writePicture(XWPFParagraph para, DocPicture picture) {
        try (InputStream is = IOUtils.openFileInputStream(picture.getFile())) {
            para.createRun().addPicture(is, picture.getPictureType(),
                    picture.getDesc(), Units.toEMU(picture.getWidth()), Units.toEMU(picture.getHeight()));
        } catch (Exception e) {
            throw new WordException("在段落中追加图片时发生异常:", e);
        }
    }

    /**
     * 渲染表格，根据表中的某个空行，抓取其中的样式，进行单元格复制，复制过程中，单元格内部的元素也会复制。
     *
     * @param table    -
     * @param styleIdx 取哪一行的样式
     * @param startRow 从第几行开始
     * @param rowCnt   渲染多少行表格
     */
    public static void renderTable(XWPFTable table, int styleIdx, int startRow, int rowCnt) {
        XWPFTableRow styleRow = table.getRow(styleIdx);
        List<XWPFTableCell> styleCells = styleRow.getTableCells();
        for (int i = startRow + 1, len = rowCnt + startRow; i < len; i++) {
            XWPFTableRow newRow = table.insertNewTableRow(i);
            newRow.getCtRow().setTrPr(styleRow.getCtRow().getTrPr());

            for (XWPFTableCell styleCell : styleCells) {
                XWPFTableCell newCell = newRow.addNewTableCell();
                //列属性
                newCell.getCTTc().setTcPr(styleCell.getCTTc().getTcPr());
                XWPFParagraph stylePara = styleCell.getParagraphs().get(0);
                //段落属性 - 未处理图片
                if (null != stylePara) {
                    XWPFParagraph newPara = newCell.getParagraphs().get(0);
                    newPara.getCTP().setPPr(stylePara.getCTP().getPPr());
                    List<XWPFRun> runs = stylePara.getRuns();
                    if (runs != null && !runs.isEmpty()) {
                        XWPFRun cell = newPara.createRun();
                        cell.setText(styleCell.getText());
                        cell.setBold(runs.get(0).isBold());
                    } else {
                        newCell.setText(styleCell.getText());
                    }
                } else {
                    newCell.setText(styleCell.getText());
                }
            }
        }
    }

    /**
     * 清空表格内部所有的行
     *
     * @param table 表格
     */
    public static void clearTable(XWPFTable table) {
        List<XWPFTableRow> rows = table.getRows();
        int rowLength = rows.size();
        for (int i = 0; i < rowLength; i++) {
            table.removeRow(0);
        }
    }

    /**
     * 渲染ListMap到Table，
     * <p>
     * 功能类似Excel导出，此函数不需要占位符，直接按照keys值顺序取值，填充到Table中。
     * Table中需要一行数据作为样式模版。
     *
     * @param table    -
     * @param styleIdx 取哪一行的样式
     * @param startRow 从第几行开始(包含startRow)
     * @param keys     map.key
     * @param list     data(String、DocPicture)
     */
    public static void renderTable(XWPFTable table, int styleIdx, int startRow, String[] keys, List<? extends Map<String, ?>> list) {
        XWPFTableRow styleRow = table.getRow(styleIdx);
        List<XWPFTableCell> styleCells = styleRow.getTableCells();
        Object value;
        Map<String, ?> map = list.get(0);
        //渲染首行数据
        for (int j = 0; j < keys.length; j++) {
            value = map.get(keys[j]);
            if (value == null) {
                styleCells.get(j).setText("");
            } else {
                styleCells.get(j).setText(Converter.convert(value, String.class));
            }
        }
        //引用首行的样式进行复制
        int len = list.size();
        if (len > 0) {
            for (int i = 0; i < len; i++) {
                map = list.get(i);
                XWPFTableRow newRow = table.insertNewTableRow(i + startRow);
                newRow.getCtRow().setTrPr(styleRow.getCtRow().getTrPr());
                for (int j = 0; j < keys.length; j++) {
                    XWPFTableCell newCell = newRow.addNewTableCell();
                    value = map.get(keys[j]);
                    if (value == null) {
                        newCell.setText("");
                    } else {
                        newCell.getCTTc().setTcPr(styleCells.get(j).getCTTc().getTcPr());
                        if (value instanceof DocPicture) {
                            //填充图片
                            XWPFParagraph newPara = newCell.getParagraphs().get(0);
                            DocPicture picture = (DocPicture) value;
                            writePicture(newPara, picture);
                        } else {
                            //填充文本
                            newCell.setText(Converter.convert(value, String.class));
                        }
                    }
                }
            }
        }
    }


    /**
     * word单元格列合并
     *
     * @param table     表格
     * @param row       合并列所在行
     * @param startCell 开始列
     * @param endCell   结束列
     * @param pid       合并单元格之后，需要清除数据，观察自己word中，文字处于第几个p标签
     */
    public static void mergeCellsHorizontal(XWPFTable table, int row, int startCell, int endCell, int pid) {
        for (int i = startCell; i <= endCell; i++) {
            XWPFTableCell cell = table.getRow(row).getCell(i);
            if (i == startCell) {
                // The first merged cell is set with RESTART merge value
                cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.RESTART);
            } else {
                // Cells which join (merge) the first one, are set with CONTINUE
                cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.CONTINUE);
                cell.getCTTc().getPArray(pid).removeR(0);
            }
        }
    }

    /**
     * word单元格行合并
     *
     * @param table    表格
     * @param col      合并行所在列
     * @param startRow 开始行
     * @param endRow   结束行
     * @param pid      合并单元格之后，需要清除数据，观察自己word中，文字处于第几个p标签
     */
    public static void mergeCellsVertically(XWPFTable table, int col, int startRow, int endRow, int pid) {
        for (int i = startRow; i <= endRow; i++) {
            XWPFTableCell cell = table.getRow(i).getCell(col);
            if (i == startRow) {
                // The first merged cell is set with RESTART merge value
                cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.RESTART);
            } else {
                // Cells which join (merge) the first one, are set with CONTINUE
                cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.CONTINUE);
                cell.getCTTc().getPArray(pid).removeR(0);
            }
        }
    }
}
